package dao;


import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

// 通过这个类来封装针对 file_meta 表的操作.
public class FileDao {


        // 1. 初始化数据库 (建表)
    public void initDB() {
        // 1) 先能够读取到 db.sql 中的 SQL 语句.
        // 2) 根据 SQL 语句调用 jdbc 执行操作.
        Connection connection = null;
        Statement statement = null;
        try {
            connection = DBUtil.getConnection();
            // 建立连接之后，构造并执行 sql 语句
            statement = connection.createStatement();
            String[] sqls = getInitSQL();
            for (String sql : sqls) {
                System.out.println("[initDB()] sql: " + sql);
                // 之前都是 先构造好一个 statement,然后直接调用一个 无参数版本的statement.executeUpdate();
                statement.executeUpdate(sql);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 最后不要忘记释放资源
            DBUtil.close(connection,statement,null);
        }
    }

    // 从 db.sql 文件中读取文件内容
    // 虽然现在只有一个 sql 语句，但是原则上也应该是支持多个语句的
    // 返回值就设定成一个 String[]. 每个元素都是一个独立的 SQL 语句了
    private String[] getInitSQL() {

        // 用这个 StringBuilder 来存储最终的结果
        StringBuilder stringBuilder = new StringBuilder();

        // 此处需要能够动态的获取到 db.sql 文件的路径. 而不要直接写死一个绝对路径.
        // FileDao.class 这是得到这个类的类对象。FileDao.class.getClassLoader() 这是得到类对象对应的类加载器
        // try 后面的() ：作用是可以关闭其中的资源，在try的括号种进行创建资源连接的写法
        // 这种写法会在try语句结束后自动释放，前提是这些可关闭的资源必须实现 java.lang.AutoCloseable 接口。
        try(InputStream inputStream = FileDao.class.getClassLoader().getResourceAsStream("db.sql")) {
            // InputStream OutputStream 针对的是二进制文件 （字节流）
            // Writer Reader 针对的是文本文件 （字符流）
            // db.sql 是文本文件，所以我们需要把 inputStream 转换成读取文本文件的类
            // 这是把 inputStream 转换成读取文本文件的类
            try(InputStreamReader inputStreamReader = new InputStreamReader(inputStream,"utf8")) {
                // 注意此处的 ch 返回值，虽然是一个 int，但实际上是一个 char 类型
                // 只不过是为了可以有 -1 这个特殊的数字，来表示读取结束，所以使用 int 类型，可以转换为 char
                /**
                 * 注意下面那句代码应该是在 循环里面的,不能让他一直读下去 ,代码位置错误,会一直读下去导致 堆的空间满了,报错
                 */
                // 注意下面那句代码应该是在 循环里面的,不能让他一直读下去
                // int ch = inputStreamReader.read();
                while(true) {
                    int ch = inputStreamReader.read();
                    if(-1 == ch) {
                        // 文件读取完毕
                        break;
                    }
                    stringBuilder.append((char)ch);
                }
            }
//        } catch (Exception e) {
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 支持多个 sql 语句，把文件内容分成多个 sql 语句，之后 jdbc 执行的时候，一次执行一条
        // 利用 ; 来分割，得到的就是 字符串数组
        return stringBuilder.toString().split(";");
    }

    /**
     * 汤老湿代码
     * @param fileMetas
     */
//    // 1. 初始化数据库 (建表)
//    public void initDB() {
//        // 1) 先能够读取到 db.sql 中的 SQL 语句.
//        // 2) 根据 SQL 语句调用 jdbc 执行操作.
//        Connection connection = null;
//        Statement statement = null;
//        try {
//            connection = DBUtil.getConnection();
//            statement = connection.createStatement();
//            String[] sqls = getInitSQL();
//            for (String sql : sqls) {
//                System.out.println("[initDB] sql: " + sql);
//                statement.executeUpdate(sql);
//            }
//        } catch (SQLException e) {
//            e.printStackTrace();
//        } finally {
//            DBUtil.close(connection, statement, null);
//        }
//    }
//
//    // 从 db.sql 中读取文件内容.
//    // 虽然当前 db.sql 中只有一个 SQL 语句, 但是原则上写了多个也是要应该支持的.
//    // 返回值就设定成一个 String[]. 每个元素都是一个独立的 SQL 语句了.
//    private String[] getInitSQL() {
//        // 用这个 StringBuilder 来存储最终结果.
//        StringBuilder stringBuilder = new StringBuilder();
//        // 此处需要能够动态的获取到 db.sql 文件的路径. 而不要直接写死一个绝对路径.
//        try(InputStream inputStream = FileDao.class.getClassLoader().getResourceAsStream("db.sql")) {
//            try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf8")) {
//                while (true) {
//                    // 注意, 此处的 ch 返回值, 虽然是 int, 单实际上是一个 char
//                    // 只不过为了表示 -1 这个特殊的结束标记, 使用 int 类型作为返回值
//                    int ch = inputStreamReader.read();
//                    if (ch == -1) {
//                        // 文件读取完毕.
//                        break;
//                    }
//                    stringBuilder.append((char)ch);
//                }
//            }
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
//        return stringBuilder.toString().split(";");
//    }
//


    // 2. 插入文件/目录数据到数据库中
    //    这里提供一个 "批量插入" 的方法. 利用事务来批量执行
    public void add(List<FileMeta> fileMetas) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            // 建立连接
            connection = DBUtil.getConnection();
            // 1.开启事务，也就是把连接的自动提交功能关闭
            connection.setAutoCommit(false);
            // 2.针对每个要执行的 sql 语句，使用 提供的 addBatch 方法来进行累计
            String sql = "insert into file_meta values(null,?,?,?,?,?,?,?)";
            statement = connection.prepareStatement(sql);

            for (FileMeta fileMeta : fileMetas) {
                statement.setString(1, fileMeta.getName());
                statement.setString(2, fileMeta.getPath());
                statement.setBoolean(3,fileMeta.isDirectory());
                statement.setString(4, fileMeta.getPinyin());
                statement.setString(5, fileMeta.getPinyinFirst());
                // bigInt 对应的是 long
                statement.setLong(6,fileMeta.getSize());
                // 注意这个地方还需要进行包装一下
                statement.setTimestamp(7,new Timestamp(fileMeta.getLastModified()));

                statement.addBatch();


                /**
                 * 新添加一段日志
                 */
                System.out.println("[insert] 插入文件: " + fileMeta.getPath() + File.separator + fileMeta.getName());
            }
            // 3.添加完所有要执行的 Batch 之后，统一的进行 executeBatch
            statement.executeBatch();
            // 4.执行完毕之后, 通过 commit 告知数据库, 添加完毕!!
            connection.commit();
        } catch (SQLException e) {
            // 5.如果上述代码出现异常, 就要进行回滚操作
            try {
                // 防止一开始就连接失败，导致空指针异常
                if(connection != null) {
                    connection.rollback();
                }
            } catch (SQLException e2) {
                e2.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            DBUtil.close(connection,statement,null);
        }
    }

    // 3. 按照特定的关键词进行查询.
    //    这个是在实现文件搜索功能的时候, 必备的部分.
    //    此处查询的这个 pattern (原意是模式，指特定的规则), 可能是文件名的一部分, 也可能是文件名拼音的一部分, 还可能是拼音首字母的一部分....
    public List<FileMeta> searchByPattern(String pattern) {
        List<FileMeta> fileMetas = new ArrayList<>();

        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            // 1.建立连接
            connection = DBUtil.getConnection();
            // 2.构造 sql 语句
            String sql = "select name,path,is_directory,size,last_modified from file_meta"
                    // 模糊查询,注意 where 前面留有空格
                    // + "where name like ? or pinyin like ? or pinyin_first like ?"
                    + " where name like ? or pinyin like ? or pinyin_first like ?"
                    // 按照路径和名字进行排序
                    + " order by path , name";
            statement = connection.prepareStatement(sql);
            statement.setString(1,"%" + pattern + "%");
            statement.setString(2,"%" + pattern + "%");
            statement.setString(3,"%" + pattern + "%");

            // 3.执行 sql 语句
            resultSet = statement.executeQuery();
            // 4.查询的话，多了一步遍历操作
            while (resultSet.next()) {
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean isDirectory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp lastModified = resultSet.getTimestamp("last_modified");
                // 构造一个 FileMeta 对象里去
                // FileMeta fileMeta = new FileMeta(name,path,isDirectory,size,lastModified);
                // 注意构造方法里是 long 类型，.getTime() 获取里面的时间戳，并且可以转换为 long 类型,
                FileMeta fileMeta = new FileMeta(name,path,isDirectory,size,lastModified.getTime());
                fileMetas.add(fileMeta);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5.释放资源
            DBUtil.close(connection,statement,resultSet);
        }
        return fileMetas;
    }

    // 4. 给定路径, 查询这个路径对应的结果. (这个路径下都有哪些文件)  和 上面的全部查询步骤差不多，只是 sql 语句不同
    //    这个方法, 会在后续重新扫描, 更新数据库的时候用到
    public List<FileMeta> searchByPath(String targetPath) {
        List<FileMeta> fileMetas = new ArrayList<>();

        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            // 1.建立连接
            connection = DBUtil.getConnection();
            // 2.构造 sql 语句
            String sql = "select name,path,is_directory,size,last_modified from file_meta"
                    // 注意 where 前面要加上一个空格,不然会和前面的单词连在一块
                    // + "where path = ?"
                    + " where path = ?"
                    + "order by path , name";
            statement = connection.prepareStatement(sql);
            statement.setString(1,targetPath);

            // 3.执行 sql 语句
            resultSet = statement.executeQuery();
            // 4.查询的话，多了一步遍历操作
            while (resultSet.next()) {
                String name = resultSet.getString("name");
                String path = resultSet.getString("path");
                boolean isDirectory = resultSet.getBoolean("is_directory");
                long size = resultSet.getLong("size");
                Timestamp lastModified = resultSet.getTimestamp("last_modified");
                // 构造一个 FileMeta 对象里去
                // FileMeta fileMeta = new FileMeta(name,path,isDirectory,size,lastModified);
                // 注意构造方法里是 long 类型，.getTime() 获取里面的时间戳，并且可以转换为 long 类型,
                FileMeta fileMeta = new FileMeta(name,path,isDirectory,size,lastModified.getTime());
                fileMetas.add(fileMeta);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 5.释放资源
            DBUtil.close(connection,statement,resultSet);
        }
        return fileMetas;
    }

    // 5. 删除数据
    //    发现某个文件已经从磁盘上删掉了, 此时就需要把表里的内容也进行更新.
    //    进行删除的时候, 可能当前被删除的是一个普通文件(直接删除对应的表记录就行了)
    //    也有可能删除的时候, 被删除的是一个目录, 此时, 就需要把目录里包含的子文件/子目录也都统一删除掉.
    public void delete(List<FileMeta> fileMetas) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            // 1.建立连接
            connection = DBUtil.getConnection();
            // 开启事务，也就是把自动提交功能给关闭
            connection.setAutoCommit(false);
            // 2.构造 sql 语句
            String sql = null;
            for (FileMeta fileMeta : fileMetas) {
                if(!fileMeta.isDirectory()) {
                    // 普通文件的删除 (可以根据 id ，也可以根据名字和路径)
                    // sql = "delete from file_meta where id = ?";
                    sql = "delete from file_meta where name = ? and path = ?";
                } else {
                    // 文件夹的删除
                    // 针对目录的删除操作
                    // 例如, 当前要删除的 path 是 d:/test
                    // 此处 path like ? 要替换成形如 'd:/test%' => 目的就是把当前这个被删除的目录下面的子文件和子目录都删掉.
                    sql = "delete from file_meta where (name = ? and path = ?) or path = ?";
                }

                // 此处就不能像前面的 add 一样, 使用 addBatch 了. addBatch 前提是, sql 是一个模板
                // 把 ? 替换成不同的值. 此处 sql 是不一定相同的.
                // 因此此处就需要重新构造出 statement 对象来表示这个 SQL 了
                statement = connection.prepareStatement(sql);
                if(!fileMeta.isDirectory()) {
                    // 普通文件的删除
                    statement.setString(1, fileMeta.getName());
                    statement.setString(2, fileMeta.getPath());
                } else {
                    // 文件夹的删除
                    statement.setString(1, fileMeta.getName());
                    statement.setString(2, fileMeta.getPath());
                    // File.separator 是 Java 中的一个常量，表示文件路径分隔符。
                    // 在 Windows 系统中，文件路径分隔符为“\”，
                    // 而在 Unix/Linux 系统中，文件路径分隔符为“/”。
                    // 注意删除文件夹下的文件时，要带上自己的名字
                    statement.setString(3, fileMeta.getPath() + File.separator + fileMeta.getName() + File.separator + "%");
                }
                // 真正执行这里的删除操作
                statement.executeUpdate();
                System.out.println("[delete] " + fileMeta.getPath() + fileMeta.getName());

                /**
                 *  // 注意!! 此处代码中是有多个 Statement 对象的. 每个对象都得关闭一次.
                 *  // 注意!! 此处代码是在 for 循环中是有多个 Statement 对象的. 每个对象都得关闭一次.
                 *  // 和 add 方法不同的是 ， 上面那个 statement 对象是在 for 循环外面，只有一个对象，所以只关闭一次就可以了
                 */
                 statement.close();

            }
            // 告诉数据库，事务执行完毕了
            connection.commit();
        } catch (SQLException e) {
            e.printStackTrace();
            try {
                connection.rollback();
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
        } finally {
            DBUtil.close(connection,statement,null);
        }
    }

}
